// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview This implements a table header. */ cr.define('cr.ui.table', function() { /** @const */ var TableSplitter = cr.ui.TableSplitter; /** * Creates a new table header. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {HTMLDivElement} */ var TableHeader = cr.ui.define('div'); TableHeader.prototype = { __proto__: HTMLDivElement.prototype, table_: null, /** * Initializes the element. */ decorate: function() { this.className = 'table-header'; this.headerInner_ = this.ownerDocument.createElement('div'); this.headerInner_.className = 'table-header-inner'; this.appendChild(this.headerInner_); this.addEventListener('touchstart', this.handleTouchStart_.bind(this), false); }, /** * Updates table header width. Header width depends on list having a * vertical scrollbar. */ updateWidth: function() { // Header should not span over the vertical scrollbar of the list. var list = this.table_.querySelector('list'); this.headerInner_.style.width = list.clientWidth + 'px'; }, /** * Resizes columns. */ resize: function() { var cm = this.table_.columnModel; var headerCells = this.querySelectorAll('.table-header-cell'); if (headerCells.length != cm.size) { this.redraw(); return; } for (var i = 0; i < cm.size; i++) { headerCells[i].style.width = cm.getWidth(i) + 'px'; } this.placeSplitters_(this.querySelectorAll('.table-header-splitter')); }, batchCount_: 0, startBatchUpdates: function() { this.batchCount_++; }, endBatchUpdates: function() { this.batchCount_--; if (this.batchCount_ == 0) this.redraw(); }, /** * Redraws table header. */ redraw: function() { if (this.batchCount_ != 0) return; var cm = this.table_.columnModel; var dm = this.table_.dataModel; this.updateWidth(); this.headerInner_.textContent = ''; if (!cm || ! dm) { return; } for (var i = 0; i < cm.size; i++) { var cell = this.ownerDocument.createElement('div'); cell.style.width = cm.getWidth(i) + 'px'; cell.className = 'table-header-cell'; if (dm.isSortable(cm.getId(i))) cell.addEventListener('click', this.createSortFunction_(i).bind(this)); cell.appendChild(this.createHeaderLabel_(i)); this.headerInner_.appendChild(cell); } this.appendSplitters_(); }, /** * Appends column splitters to the table header. */ appendSplitters_: function() { var cm = this.table_.columnModel; var splitters = []; for (var i = 0; i < cm.size; i++) { // splitter should use CSS for background image. var splitter = new TableSplitter({table: this.table_}); splitter.columnIndex = i; splitter.addEventListener('dblclick', this.handleDblClick_.bind(this, i)); this.headerInner_.appendChild(splitter); splitters.push(splitter); } this.placeSplitters_(splitters); }, /** * Place splitters to right positions. * @param {Array.|NodeList} splitters Array of splitters. */ placeSplitters_: function(splitters) { var cm = this.table_.columnModel; var place = 0; for (var i = 0; i < cm.size; i++) { place += cm.getWidth(i); splitters[i].style.webkitMarginStart = place + 'px'; } }, /** * Renders column header. Appends text label and sort arrow if needed. * @param {number} index Column index. */ createHeaderLabel_: function(index) { var cm = this.table_.columnModel; var dm = this.table_.dataModel; var labelDiv = this.ownerDocument.createElement('div'); labelDiv.className = 'table-header-label'; if (cm.isEndAlign(index)) labelDiv.style.textAlign = 'end'; var span = this.ownerDocument.createElement('span'); span.appendChild(cm.renderHeader(index, this.table_)); span.style.padding = '0'; if (dm) { if (dm.sortStatus.field == cm.getId(index)) { if (dm.sortStatus.direction == 'desc') span.className = 'table-header-sort-image-desc'; else span.className = 'table-header-sort-image-asc'; } } labelDiv.appendChild(span); return labelDiv; }, /** * Creates sort function for given column. * @param {number} index The index of the column to sort by. */ createSortFunction_: function(index) { return function() { this.table_.sort(index); }.bind(this); }, /** * Handles the touchstart event. If the touch happened close enough * to a splitter starts dragging. * @param {TouchEvent} e The touch event. */ handleTouchStart_: function(e) { if (e.touches.length != 1) return; var clientX = e.touches[0].clientX; var minDistance = TableHeader.TOUCH_DRAG_AREA_WIDTH; var candidate; var splitters = this.querySelectorAll('.table-header-splitter'); for (var i = 0; i < splitters.length; i++) { var r = splitters[i].getBoundingClientRect(); if (clientX <= r.left && r.left - clientX <= minDistance) { minDistance = r.left - clientX; candidate = splitters[i]; } if (clientX >= r.right && clientX - r.right <= minDistance) { minDistance = clientX - r.right; candidate = splitters[i]; } } if (candidate) candidate.startDrag(clientX, true); // Splitter itself shouldn't handle this event. e.stopPropagation(); }, /** * Handles the double click on a column separator event. * Ajusts column width. * @param {number} index Column index. * @param {Event} e The double click event. */ handleDblClick_: function(index, e) { this.table_.fitColumn(index); } }; /** * The table associated with the header. * @type {cr.ui.Table} */ cr.defineProperty(TableHeader, 'table'); /** * Rectangular area around the splitters sensitive to touch events * (in pixels). */ TableHeader.TOUCH_DRAG_AREA_WIDTH = 30; return { TableHeader: TableHeader }; });